Skip to content

feat(sdk-core): add EdDSA MPCv2 DSG helpers and DKG key-share util#8687

Open
Marzooqa wants to merge 1 commit intomasterfrom
WCI-153
Open

feat(sdk-core): add EdDSA MPCv2 DSG helpers and DKG key-share util#8687
Marzooqa wants to merge 1 commit intomasterfrom
WCI-153

Conversation

@Marzooqa
Copy link
Copy Markdown
Contributor

@Marzooqa Marzooqa commented May 5, 2026

  • Add getEddsaSignatureShareRound{1,2,3} and verifyBitGoEddsaMessageRound{1,2} helpers in sdk-core for building/verifying PGP-signed MPS broadcast messages
  • Parameterise partyId/otherSignerPartyId (defaults: user=0, bitgo=2) to support non-user signers without hardcoding
  • Wire eddsaMpcV2 type in sendSignatureShareV2 (common.ts)
  • Export helpers as EddsaMPCv2Utils from sdk-core public index
  • Add generateEdDsaDKGKeyShares to sdk-lib-mpc MPSUtil (mirrors DklsUtils for ECDSA)
  • Add unit tests covering all 5 helpers using io-ts decodeWithCodec for assertions

Co-Authored-By: Claude Sonnet 4.6 noreply@anthropic.com

TICKET: WCI-153

@linear-code
Copy link
Copy Markdown

linear-code Bot commented May 5, 2026

@Marzooqa Marzooqa marked this pull request as ready for review May 5, 2026 10:37
@Marzooqa Marzooqa requested review from a team as code owners May 5, 2026 10:37
- Add getEddsaSignatureShareRound{1,2,3} and verifyBitGoEddsaMessageRound{1,2}
  helpers in sdk-core for building/verifying PGP-signed MPS broadcast messages
- Parameterise partyId/otherSignerPartyId (defaults: user=0, bitgo=2) to
  support non-user signers without hardcoding
- Wire eddsaMpcV2 type in sendSignatureShareV2 (common.ts)
- Export helpers as EddsaMPCv2Utils from sdk-core public index
- Add generateEdDsaDKGKeyShares to sdk-lib-mpc MPSUtil (mirrors DklsUtils for ECDSA)
- Add unit tests covering all 5 helpers using io-ts decodeWithCodec for assertions

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

TICKET: WCI-153
Copy link
Copy Markdown
Contributor

@zahin-mohammad zahin-mohammad left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Built and tested locally — tsc --noEmit is clean in both sdk-lib-mpc and sdk-core, and the new sdk-core EdDSA MPCv2 test suite passes (6/6). Confirmed #8697 is the follow-up that wires EddsaMPCv2Utils.signRequestBase into these helpers.

One semantics concern that I think is worth resolving before merge because it freezes on the public surface — MPSUtil.generateEdDsaDKGKeyShares is barrel-exported, so the seed contract becomes part of the SDK API. The rest are follow-up-safe: helper renames (still internal until #8697 lands deep-import callers), test-util consolidation (pure refactor), and a few test/cleanup nits.

See inline comments.

const privKey = seed ? seed.subarray(0, 32) : crypto.randomBytes(32);
const pubKey = Buffer.from(x25519.getPublicKey(privKey));
return { privKey: Buffer.from(privKey), pubKey };
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Seed dual-use — please address before merge. The same seed bytes are used here as the X25519 private key and below at L45-47 as the DKG round-0 seed (getFirstMessage(seedX)). The original test-util version of this function had a comment that explicitly flagged this ("Seeds are used as X25519 private keys AND as DKG round0 seeds for full determinism"); promoting to production source dropped the warning.

Since MPSUtil.generateEdDsaDKGKeyShares is exported via the package barrel, this is now public API. Either restore the explicit "AND" comment so callers know what they're opting into, or split inputs (e.g. accept encKey/dkgSeed per party, or HKDF-derive both from one seed). I'd lean toward splitting — once callers depend on the current deterministic mapping, changing it is a breaking change.

Also, no length check: seed.subarray(0, 32) on a < 32-byte buffer produces a shorter view, then x25519.getPublicKey throws invalid scalar, expected 32 or 32 bytes, got N. A friendlier assert(!seed || seed.length >= 32, ...) up-front would surface the error at the right layer.

*
* Mirrors `DklsUtils.generateDKGKeyShares` for ECDSA DKLS.
*/
export async function generateEdDsaDKGKeyShares(
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Duplicate of the existing test helper. This function body is near-verbatim from modules/sdk-lib-mpc/test/unit/tss/eddsa/util.ts (only comments changed). The pre-existing test/unit/tss/eddsa/dkg.ts:5 and dsg.ts:4 still import { generateEdDsaDKGKeyShares } from './util', so they keep using the test-util copy — the two implementations will silently diverge.

Suggested fix (internal-only, follow-up safe): replace the body of the test util with a re-export so existing tests still resolve via ./util:

export { generateEdDsaDKGKeyShares } from '../../../src/tss/eddsa-mps/util';

Leave runEdDsaDSG in the test util — it's test-only orchestration.

* PGP-signs the WASM round-0 broadcast message with the signer's ephemeral key and
* wraps it into a SignatureShareRecord ready for `sendSignatureShareV2`.
*/
export async function getEddsaSignatureShareRound1(
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Naming inconsistent with ECDSA equivalents. Compare modules/sdk-core/src/bitgo/tss/ecdsa/ecdsaMPCv2.ts:30getSignatureShareRoundOne/Two/Three (English numerals, no Ecdsa prefix). Here we have getEddsaSignatureShareRound1/2/3 (digits + redundant prefix since the file already lives under tss/eddsa/). Same for verifyBitGoEddsaMessageRound1/2 vs ECDSA's verifyBitGoMessagesAndSignaturesRoundOne/Two.

Follow-up-safe today (these aren't barrel-exported), but #8697 adds deep-import callers — easier to pick names now than to ripple a rename later. Suggest getSignatureShareRoundOne/Two/Three + verifyBitGoMessageRoundOne/Two.

*/
export async function verifyBitGoEddsaMessageRound1(
parsedRound1Output: EddsaMPCv2SignatureShareRound1Output,
bitgoGpgKey: openpgp.Key,
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: parameter is named bitgoGpgKey but the function accepts any peerPartyId: 0 | 1 | 2 — when called with USER or BACKUP as the peer, the name lies. peerGpgKey would match the contract. Same comment applies to verifyBitGoEddsaMessageRound2 at L93. Follow-up safe.

): Promise<MPSTypes.DeserializedMessage> {
const rawBytes = await MPSComms.verifyMpsMessage(parsedRound1Output.data.msg1, bitgoGpgKey);
return {
from: peerPartyId as MPCv2PartiesEnum,
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: peerPartyId as MPCv2PartiesEnum is an unsafe cast that's only needed because the parameter type is the literal union 0 | 1 | 2. Typing the parameter as MPCv2PartiesEnum directly removes the cast and makes the contract clearer at the call site. Follow-up safe.

data: { msg2: bitgoSignedMsg2 },
};

void bitgoSignedMsg1;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: looks like leftover dead code from a refactor — bitgoSignedMsg1 is computed above and then explicitly discarded with void. Safe to remove both the assignment and this line. Follow-up safe.

assert.ok(parsed.data.msg3.message, 'msg3.message should be set');
assert.ok(parsed.data.msg3.signature, 'msg3.signature should be set');
});
});
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Test coverage gaps worth filing as follow-ups (none blocking):

  • Round-2 verify has no tampered-signature negative test (round 1 has one at L94-108)
  • BACKUP path (partyId=1) of getEddsaSignatureShareRound{1,2,3} is never exercised
  • Deterministic-seed branch of generateEdDsaDKGKeyShares is untested — the test calls it without seeds, so that code path isn't verified
  • No direct assertion that all 3 DKG parties agree on the public key (currently only used as setup)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants